package uk.ac.lkl.common.associator.ui;



import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;

import uk.ac.lkl.common.associator.Associator;
import uk.ac.lkl.common.associator.event.AssociatorAdapter;
import uk.ac.lkl.common.associator.event.AssociatorEvent;
import uk.ac.lkl.common.associator.ui.BasicAssociatorView;
import uk.ac.lkl.common.associator.ui.HandlePanel;
import uk.ac.lkl.common.associator.ui.HandlePanelLink;
import uk.ac.lkl.common.associator.ui.HandleSet;
import uk.ac.lkl.common.ui.ViewCreator;
import uk.ac.lkl.common.util.ds.*;
import uk.ac.lkl.common.util.ds.event.*;

import java.awt.geom.*;


public class BasicAssociatorView<L, A, R> extends JPanel {

    /**
     * An internal class for listening to the notifying lists and creating
     * appropriate handle panels.
     * 
     * @author Darren Pearce
     * @version $Revision$
     * @version $Date$
     * 
     */
    private class CollectionHandler<O> implements ListListener<O> {

        /**
         * The column in the view's GridBagLayout that the view-created
         * components should be placed.
         * 
         */
        private int column;


        /**
         * The view creator to use to create the new list element.
         * 
         */
        private ViewCreator<O> viewCreator;


        /**
         * Boolean indicating whether the handlepanel should be created on the
         * left or right.
         * 
         */
        private boolean leftHandle;


        public CollectionHandler(ViewCreator<O> viewCreator, int column,
                boolean leftHandle) {
            this.viewCreator = viewCreator;
            this.column = column;
            this.leftHandle = leftHandle;
        }


        /**
         * Process adding of an element to the collection.
         * 
         */
        public void elementAdded(ListEvent<O> e) {
            Collection<O> collection = e.getList();
            O model = e.getElement();

            Component view = createView(model, viewCreator);

            // Allow space for header
            int row = collection.size();

            Component viewPanel = addView(model, view, row, column);
            viewMap.put(model, viewPanel);

            int handleColumn = leftHandle ? column + 1 : column - 1;

            HandlePanel handlePanel = new HandlePanel(BasicAssociatorView.this,
                    1, model, leftHandle);

            if (leftHandle)
                handleSet.addLeftHandle(model, handlePanel);
            else
                handleSet.addRightHandle(model, handlePanel);

            c.gridx = handleColumn;
            c.gridy = row;
            c.weightx = 0.0f;
            add(handlePanel, c);

            validate();
        }


        public void elementMoved(ListEvent<O> e) {
            HandlePanel handlePanel;
            if (leftHandle)
                handlePanel = handleSet.getLeftHandle(e.getElement());
            else
                handlePanel = handleSet.getRightHandle(e.getElement());

            Component viewComponent = viewMap.get(e.getElement());

            GridBagConstraints handleConstraints = layout
                    .getConstraints(handlePanel);
            GridBagConstraints viewConstraints = layout
                    .getConstraints(viewComponent);

            int newIndex = e.getNewIndex();
            handleConstraints.gridy = newIndex + 1;
            viewConstraints.gridy = newIndex + 1;

            layout.setConstraints(handlePanel, handleConstraints);
            layout.setConstraints(viewComponent, viewConstraints);
            handlePanel.invalidate();
            viewComponent.invalidate();

            validate();

            // hack
            getParent().repaint();
        }


        public void elementRemoved(ListEvent<O> e) {
            HandlePanel handlePanel;
            if (leftHandle)
                handlePanel = handleSet.getLeftHandle(e.getElement());
            else
                handlePanel = handleSet.getRightHandle(e.getElement());

            remove(handlePanel);

            Component viewComponent = viewMap.get(e.getElement());
            remove(viewComponent);
            invalidate();
            validate();
            repaint();
        }

    }

    /**
     * A listener that just sets a field depending on the source of the most
     * recent entered event.
     * 
     * Currently used to implement highlighting.
     * 
     * @author Darren Pearce
     * @version $Revision$
     * @version $Date$
     * 
     */
    private class HighlightListener extends MouseAdapter {

        private Component component;


        public void mouseEntered(MouseEvent e) {
            this.component = (Component) e.getSource();
        }


        public void mouseExited(MouseEvent e) {
            this.component = null;
        }


        public Component getComponent() {
            return component;
        }
    };


    private HighlightListener highlightListener = new HighlightListener();


    private GridBagLayout layout;


    private GridBagConstraints c = new GridBagConstraints();


    private Associator<L, A, R> associator;


    private ArrayList<HandlePanelLink> links = new ArrayList<HandlePanelLink>();


    private HandleSet handleSet;


    private ViewCreator<L> leftViewCreator;


    private ViewCreator<R> rightViewCreator;


    private HashMap<Object, Component> viewMap = new HashMap<Object, Component>();


    public BasicAssociatorView(Associator<L, A, R> associator,
            ViewCreator<L> leftViewCreator, ViewCreator<R> rightViewCreator) {
        this.associator = associator;
        this.leftViewCreator = leftViewCreator;
        this.rightViewCreator = rightViewCreator;

        handleSet = new HandleSet();

        initialiseLayout();
        addListeners();
    }


    private void initialiseLayout() {
        layout = new GridBagLayout();
        setLayout(layout);
        c.fill = GridBagConstraints.BOTH;

        addMouseHandler();

        c.gridx = 0;
        c.gridy = 0;
        c.gridwidth = 1;
        // add(new JLabel("Source"), c);

        c.gridx = 2;
        c.gridy = 0;
        c.gridwidth = 1;
        JPanel gapPanel = new JPanel();
        gapPanel.setPreferredSize(new Dimension(200, 10));
        gapPanel.setMinimumSize(new Dimension(200, 10));
        gapPanel.setOpaque(false);
        add(gapPanel, c);

        c.gridx = 4;
        c.gridy = 0;
        c.gridwidth = 1;
        // add(new JLabel("Destination"), c);

        AbstractNotifyingList<L> leftList = associator.getLeftList();
        AbstractNotifyingList<R> rightList = associator.getRightList();

        layoutListComponents(leftList, leftViewCreator, 0);
        layoutHandlePanels(leftList, 1, false);

        layoutHandlePanels(rightList, 3, true);
        layoutListComponents(rightList, rightViewCreator, 4);

        synchroniseWithModel();
    }


    private void synchroniseWithModel() {
        // synch view with model
        for (L leftElement : associator.getLeftList()) {
            ArrayList<R> rightElements = associator
                    .getRightAssociations(leftElement);

            if (rightElements == null)
                continue;

            for (R rightElement : rightElements)
                processAssociationAdded(leftElement, rightElement);
        }
    }


    /**
     * 
     * 
     * @param <O>
     * @param list
     * @param viewCreator
     * @param columnIndex
     * 
     */
    private <O> void layoutListComponents(AbstractNotifyingList<O> list,
                                          ViewCreator<O> viewCreator,
                                          int columnIndex) {
        for (int i = 0; i < list.size(); i++) {
            O model = list.get(i);
            int rowIndex = i + 1; // allow for header
            Component view = createView(model, viewCreator);
            Component viewPanel = addView(model, view, rowIndex, columnIndex);
            // duplicated code
            viewMap.put(model, viewPanel);
        }
    }


    /**
     * Use the viewCreator to create the view and do any additional necessary
     * processing.
     * 
     * @param <O>
     * @param model
     * @param viewCreator
     * @return
     * 
     */
    private <O> Component createView(O model, ViewCreator<O> viewCreator) {
        Component component = viewCreator.createView(model);
        addViewListeners(component);
        return component;
    }


    private void addViewListeners(Component view) {
        view.addMouseListener(highlightListener);
    }


    public <O> void layoutHandlePanels(AbstractNotifyingList<O> list,
                                       int columnIndex,
                                       boolean handleOnLeft) {
        for (int i = 0; i < list.size(); i++) {
            int rowIndex = i + 1; // allow for header
            c.gridx = columnIndex;
            c.gridy = rowIndex;
            c.weightx = 0.0f;

            O model = list.get(i);
            HandlePanel handlePanel = new HandlePanel(this, 2, model, true);

            if (handleOnLeft)
                handleSet.addRightHandle(model, handlePanel);
            else
                handleSet.addLeftHandle(model, handlePanel);
            add(handlePanel, c);
        }
    }


    private void addListeners() {
        addAssociatorListener();
        addCollectionListeners();
    }


    private void addAssociatorListener() {
        associator.addAssociatorListener(new AssociatorAdapter<L, A, R>() {

            public void associationAdded(AssociatorEvent<L, A, R> e) {
                processAssociationAdded(e);
            }


            public void associationRemoved(AssociatorEvent<L, A, R> e) {
                processAssociationRemoved(e);
            }
        });
    }


    private void addCollectionListeners() {
        AbstractNotifyingList<L> leftList = associator.getLeftList();
        AbstractNotifyingList<R> rightList = associator.getRightList();
        addCollectionListener(leftList, leftViewCreator, 0, true);
        addCollectionListener(rightList, rightViewCreator, 4, false);
    }


    private <O> void addCollectionListener(AbstractNotifyingList<O> list,
                                           ViewCreator<O> viewCreator,
                                           int columnIndex,
                                           boolean handleOnLeft) {
        list.addListListener(new CollectionHandler<O>(viewCreator, columnIndex,
                handleOnLeft));
    }


    private void processAssociationAdded(AssociatorEvent e) {
        Object leftElement = e.getLeftElement();
        Object rightElement = e.getRightElement();
        processAssociationAdded(leftElement, rightElement);
    }


    private void processAssociationAdded(Object leftElement, Object rightElement) {
        HandlePanel leftHandle = handleSet.getLeftHandle(leftElement);
        HandlePanel rightHandle = handleSet.getRightHandle(rightElement);
        HandlePanelLink link = new HandlePanelLink(leftHandle, rightHandle);
        links.add(link);
        repaint();
    }


    private void processAssociationRemoved(AssociatorEvent e) {
        Object leftElement = e.getLeftElement();
        Object rightElement = e.getRightElement();

        HandlePanel leftHandle = handleSet.getLeftHandle(leftElement);
        HandlePanel rightHandle = handleSet.getRightHandle(rightElement);

        // todo: implement .equals for link
        HandlePanelLink link = new HandlePanelLink(leftHandle, rightHandle);

        boolean removedOk = links.remove(link);
        repaint();
    }


    // shouldn't allow this
    public HandleSet getHandleSet() {
        return handleSet;
    }


    public Associator getAssociator() {
        return associator;
    }

    private class DeletingListener extends MouseAdapter {

        private Object object;


        private JPopupMenu menu;


        private Component component;


        // todo: stop using a menu per instance!!
        public DeletingListener(Object object, Component component) {
            this.object = object;
            this.component = component;
            this.menu = new JPopupMenu();
            JMenuItem deleteItem = new JMenuItem("Delete");
            deleteItem.addActionListener(new ActionListener() {

                public void actionPerformed(ActionEvent e) {
                    delete();
                }
            });
            menu.add(deleteItem);
        }


        public void mousePressed(MouseEvent e) {
            if (e.getButton() != MouseEvent.BUTTON3)
                return;
            int height = component.getHeight();
            menu.show(component, 0, height);

        }


        // hack hack hack quick fix to make it remove both left and
        // right!
        private void delete() {
            associator.getLeftList().remove(object);
            associator.getRightList().remove(object);
        }
    };


    /**
     * Add the given view at a given row and column in this instance's
     * GridBagLayout.
     * 
     * This method wraps the component in a border. Currently no check is made
     * as to whether the component already has a border or not.
     * 
     * @param view
     * @param row
     * @param column
     * 
     */
    private Component addView(Object model, Component view, int row, int column) {
        JPanel panel = new JPanel(new GridLayout(1, 1));
        panel.add(view);
        panel.setBorder(BorderFactory.createEtchedBorder());

        MouseListener listener = new DeletingListener(model, panel);

        // need to do both of these in case view has mouse listeners
        view.addMouseListener(listener);
        panel.addMouseListener(listener);

        c.gridx = column;
        c.gridy = row;
        c.weightx = 0.0;
        add(panel, c);

        return panel;
    }


    // Massive hack for now!
    public Point pressPoint = null;


    public Point dragPoint = null;


    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        drawLinks(g);

        if (pressPoint == null)
            return;

        if (dragPoint == null)
            return;

        g.drawLine(pressPoint.x, pressPoint.y, dragPoint.x, dragPoint.y);
    }


    private void drawLinks(Graphics g) {
        Graphics2D g2 = (Graphics2D) g.create();
        Component highlightedComponent = highlightListener.getComponent();
        for (HandlePanelLink link : links) {
            HandlePanel leftHandlePanel = link.getLeftHandlePanel();
            HandlePanel rightHandlePanel = link.getRightHandlePanel();

            Rectangle leftBounds = leftHandlePanel.getBounds();
            Rectangle rightBounds = rightHandlePanel.getBounds();

            int x1 = leftBounds.x + leftBounds.width;
            int y1 = leftBounds.y + leftBounds.height / 2;
            int x2 = rightBounds.x;
            int y2 = rightBounds.y + rightBounds.height / 2;
            g2.setColor(Color.black);

            // g2.setStroke(new BasicStroke(2f));

            g2.drawLine(x1, y1, x2, y2);
        }
    }


    public void addMouseHandler() {
        addMouseListener(new MouseAdapter() {

            public void mousePressed(MouseEvent e) {
                processMouseClicked(e);
            }
        });
    }


    private void processMouseClicked(MouseEvent e) {
        if (e.getClickCount() < 2)
            return;

        int x = e.getX();
        int y = e.getY();

        Double minimumDistance = null;
        HandlePanelLink minimumLink = null;

        for (HandlePanelLink link : links) {
            HandlePanel leftHandlePanel = link.getLeftHandlePanel();
            HandlePanel rightHandlePanel = link.getRightHandlePanel();

            Rectangle leftBounds = leftHandlePanel.getBounds();
            Rectangle rightBounds = rightHandlePanel.getBounds();

            int x1 = leftBounds.x + leftBounds.width;
            int y1 = leftBounds.y + leftBounds.height / 2;
            int x2 = rightBounds.x;
            int y2 = rightBounds.y + rightBounds.height / 2;

            // should cache these
            Line2D.Double line = new Line2D.Double(x1, y1, x2, y2);

            // hack for horizontal layout
            if (x < x1 || x > x2)
                continue;

            double distance = line.ptLineDistSq(x, y);

            if (minimumDistance == null) {
                minimumDistance = distance;
                minimumLink = link;
            }
            else {
                if (distance < minimumDistance) {
                    minimumDistance = distance;
                    minimumLink = link;
                }
            }
        }

        if (minimumLink == null)
            return;

        // 10 pixels
        if (minimumDistance > 10)
            return;

        HandlePanel leftHandlePanel = minimumLink.getLeftHandlePanel();
        Object leftObject = leftHandlePanel.getElement();
        HandlePanel rightHandlePanel = minimumLink.getRightHandlePanel();
        Object rightObject = rightHandlePanel.getElement();
        associator.removeAssociation((L) leftObject, (R) rightObject);
    }

}
